import axios from 'axios'
import type {
  AxiosRequestConfig,
  AxiosInstance,
  InternalAxiosRequestConfig,
  AxiosResponse
} from 'axios'
import { cloneDeep } from 'lodash-es'
import type {
  CreateAxiosOptions,
  RequestOptions,
  Result,
  UploadFileParams
} from '@/utils/http/axios/types'
import { AxiosCanceler } from '@/utils/http/axios/axiosCancel'
import { isFunction } from '@/utils/is'
import { RequestEnum, ContentTypeEnum } from '@/enums/httpEnum'

export class CustomAxios {
  private axiosInstance: AxiosInstance
  private readonly options: CreateAxiosOptions

  constructor(options: CreateAxiosOptions) {
    this.options = options
    this.axiosInstance = axios.create(options)
    this.setupInterceptors()
  }

  getAxiosInstance(): AxiosInstance {
    return this.axiosInstance
  }

  /**
   * @description 重新配置axios
   */
  configAxios(config: CreateAxiosOptions): void {
    if (!this.axiosInstance) {
      return
    }
    this.createAxios(config)
  }

  /**
   * @description 设置通用header
   * @param headers
   */
  setHeader(headers: any): void {
    if (!this.axiosInstance) {
      return
    }
    Object.assign(this.axiosInstance.defaults.headers, headers)
  }

  /**
   * @description request请求方法
   * @param config
   * @param options
   */
  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    // 就是cloneDeep的锅
    // CancelToken 对象内部包含一个可调用的 cancel 函数，该函数作为属性保存在 CancelToken 对象中。一些深拷贝函数（如 lodash.cloneDeep）可能无法正确复制这类包含可调用函数的对象，导致克隆后的 CancelToken 失去原本的取消功能。
    let conf: AxiosRequestConfig = config
    const transform = this.getTransform()
    const { requestOptions } = this.options
    const opt: RequestOptions = Object.assign({}, requestOptions, options ? options : {})
    const { beforeRequestHook, requestCatch, transformRequestData } = transform || {}
    if (beforeRequestHook && isFunction(beforeRequestHook)) {
      conf = beforeRequestHook(conf, opt)
    }

    // 这里重置为最新requestOptions
    // @ts-ignore
    conf.requestOptions = opt

    return new Promise((resolve, reject) => {
      this.axiosInstance.request<any, AxiosResponse<Result>>(conf).then(
        (res: AxiosResponse<Result>) => {
          if (transformRequestData && isFunction(transformRequestData)) {
            try {
              const ret = transformRequestData(res, opt)
              resolve(ret)
            } catch (err) {
              reject(err || new Error('request error!'))
            }
            return
          }
          resolve(res as unknown as Promise<T>)
        },
        (error: Error) => {
          // 请求是否被取消
          const isCancel = axios.isCancel(error)
          if (isCancel) {
            // 请求被取消，无需进一步处理
            return;
          }
          if (requestCatch && isFunction(requestCatch)) {
            reject(requestCatch(error))
            return
          }
          reject(error)
        }
      )
    })
  }

  get<T = any>(config: AxiosRequestConfig, options: RequestOptions): Promise<T> {
    return this.request({ ...config, method: RequestEnum.GET }, options)
  }

  post<T = any>(config: AxiosRequestConfig, options: RequestOptions): Promise<T> {
    return this.request({ ...config, method: RequestEnum.POST }, options)
  }

  put<T = any>(config: AxiosRequestConfig, options: RequestOptions): Promise<T> {
    return this.request({ ...config, method: RequestEnum.PUT }, options)
  }

  patch<T = any>(config: AxiosRequestConfig, options: RequestOptions): Promise<T> {
    return this.request({ ...config, method: RequestEnum.PATCH }, options)
  }

  delete<T = any>(config: AxiosRequestConfig, options: RequestOptions): Promise<T> {
    return this.request({ ...config, method: RequestEnum.DELETE }, options)
  }

  uploadFile<T>(config: AxiosRequestConfig, params: UploadFileParams) {
    const formData = new window.FormData()
    const customFileName = params.name || 'file'

    if (params.fileName) {
      formData.append(customFileName, params.file, params.fileName)
    } else {
      formData.append(customFileName, params.file)
    }

    if (params.data) {
      Object.keys(params.data).forEach((key) => {
        const value = params.data![key]
        if (Array.isArray(value)) {
          value.forEach((item) => formData.append(`${key}[]`, item))
          return
        }
        formData.append(key, params.data![key])
      })
    }

    // @ts-ignore
    return this.axiosInstance.request<T>({
      method: RequestEnum.POST,
      data: formData,
      headers: {
        'Content-type': ContentTypeEnum.FORM_DATA,
        ignoreCancelToken: true
      },
      ...config
    })
  }

  /**
   * @description 创建axios实例
   * @param config
   * @private
   */
  private createAxios(config: CreateAxiosOptions): void {
    this.axiosInstance = axios.create(config)
  }

  private getTransform() {
    const { transform } = this.options
    return transform
  }

  /**
   * @description 拦截器配置
   */
  private setupInterceptors() {
    const transform = this.getTransform()
    if (!transform) {
      return
    }
    const {
      requestInterceptors,
      requestInterceptorsCatch,
      responseInterceptors,
      responseInterceptorsCatch
    } = transform
    const axiosCanceler = new AxiosCanceler()

    // 请求拦截处理
    this.axiosInstance.interceptors.request.use(
      (config) => {
        const {
          headers: { ignoreCancelToken }
        } = config
        let conf = cloneDeep(config)
        const ignoreCancel =
          ignoreCancelToken !== undefined
            ? ignoreCancelToken
            : this.options.requestOptions?.ignoreCancelToken
        !ignoreCancel && axiosCanceler.addPending(config)
        if (requestInterceptors && isFunction(requestInterceptors)) {
          conf = requestInterceptors(config, this.options)
        }
        return conf
      },
      (error) => {
        requestInterceptorsCatch &&
        isFunction(requestInterceptorsCatch) &&
        requestInterceptorsCatch(error)
      }
    )

    // 响应拦截处理
    this.axiosInstance.interceptors.response.use(
      (res: AxiosResponse<any>) => {
        res && axiosCanceler.removePending(res.config)
        if (responseInterceptors && isFunction(responseInterceptors)) {
          res = responseInterceptors(res)
        }
        return res
      },
      (error) => {
        responseInterceptorsCatch &&
        isFunction(responseInterceptorsCatch) &&
        responseInterceptorsCatch(error)
      }
    )
  }
}
